package com.sp.business.normalization.impl;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;

import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.sp.generated.xml.firemon.ServiceObject;
import com.sp.generated.xml.firemon.ServiceObjectCollection;

public class ServiceKeyGenerator
{
  private static final Logger logger = LoggerFactory.getLogger(ServiceKeyGenerator.class);

  public static void generateServiceKeys(ServiceObjectCollection previous, ServiceObjectCollection current)
  {
    long start = System.currentTimeMillis();

    // key statistics
    int matchedKeyCount = 0;
    int generatedKeyCount = 0;

    List<ServiceObject> nonMatchedServices = new ArrayList<ServiceObject>(current.getServiceObjects());
    List<ServiceObject> nonMatchedPreviousServices;

	 //prevent copy duplicate key	 
    Set<String> copiedKeySet = new HashSet<String>();
    
    if (previous == null)
      nonMatchedPreviousServices = new ArrayList<ServiceObject>();
    else
      nonMatchedPreviousServices = new ArrayList<ServiceObject>(previous.getServiceObjects());

    // find exact matches first
    for (ServiceObject service : current.getServiceObjects())
    {
      ServiceObject previousService = findServiceExactMatch(nonMatchedPreviousServices, service);

      if (previousService == null)
        continue;

      copyUID(previousService, service, copiedKeySet);

      // remove the previous service, so it doesn't get matched again
      nonMatchedPreviousServices.remove(previousService);

      // remove the service, so we don't try to match it again
      nonMatchedServices.remove(service);

      matchedKeyCount++;
    }

    // now try to match remaining, non-exact-matched users
    for (ServiceObject service : nonMatchedServices)
    {
      ServiceObject previousService = findServiceCloseMatch(nonMatchedPreviousServices, service);

      // copy or generate the guid
      copyUID(previousService, service, copiedKeySet);

      if (previousService == null)
      {
        /* no close match could be found for the object.
         * the object is either a newly-created object, 
         * or it has changed so significantly that 
         * it is effectively a new object.
        */
        generatedKeyCount++;
      }
      else
      {
        // we found a close match,
        // remove the previous securityRule, so it doesn't get matched again
        nonMatchedPreviousServices.remove(previousService);

        matchedKeyCount++;
      }
    }

    logger.info(String.format("Matched %s and generated %s service keys in %s ms.", matchedKeyCount,
        generatedKeyCount, System.currentTimeMillis() - start));
  }

  protected static String copyUID(ServiceObject previous, ServiceObject current, Set<String> copiedKeySet)
  {
    String guid = null;
    
    boolean foundDuplicate = false;
    if(previous != null)
    {
		// do not copy duplicated key
        guid = previous.getUID();
        if (copiedKeySet.contains(guid)) {
    				foundDuplicate = true;
    				logger.info("found duplicate key: " + guid);
    			} else {
    				copiedKeySet.add(guid);
    			}
    }

    if (StringUtils.isBlank(guid) || !NormalizationUtil.isStringUUID(guid)
			|| foundDuplicate == true)
      guid = String.format("{%s}", UUID.randomUUID().toString().toUpperCase());

    current.setUID(guid);

    current.setXsid("ServiceObject" + guid.replaceAll("\\{", "").replaceAll("\\}", ""));

    return guid;
  }

  private static ServiceObject findServiceExactMatch(List<ServiceObject> previousServices, ServiceObject service)
  {
    for (ServiceObject previousService : previousServices)
    {
      if (isServiceExactMatch(previousService, service))
        return previousService;
    }

    return null;
  }

  private static boolean isServiceExactMatch(ServiceObject previousService, ServiceObject service)
  {
    // test name
    if (!KeyGeneratorBase.isExactMatch(previousService.getName(), service.getName()))
      return false;

    // test service type
    if (!KeyGeneratorBase.isExactMatch(previousService.getType(), service.getType()))
      return false;

    //TODO: jcoon - test netscreen service group members?
    // test port range
    if (!KeyGeneratorBase.isExactMatch(previousService.getPortRange(), service.getPortRange()))
      return false;

    // test port
    if (!KeyGeneratorBase.isExactMatch(previousService.getPort(), service.getPort()))
      return false;

    // test port range end
    if (!KeyGeneratorBase.isExactMatch(previousService.getPortRangeEnd(), service.getPortRangeEnd()))
      return false;

    // test src port
    if (!KeyGeneratorBase.isExactMatch(previousService.getSrcPort(), service.getSrcPort()))
      return false;

    // test src port range
    if (!KeyGeneratorBase.isExactMatch(previousService.getSrcPortRange(), service.getSrcPortRange()))
      return false;

    // test src port range end
    if (!KeyGeneratorBase.isExactMatch(previousService.getSrcPortRangeEnd(), service.getSrcPortRangeEnd()))
      return false;

    return true;
  }

  protected static ServiceObject findServiceCloseMatch(List<ServiceObject> previousServices, ServiceObject service)
  {
    int lowestEditDistance = Integer.MAX_VALUE;
    ServiceObject bestMatch = null;

    for (ServiceObject previousService : previousServices)
    {
      // can't change the service type, if the type changes, it's a different service
      if (!KeyGeneratorBase.isExactMatch(previousService.getType(), service.getType()))
        continue;

      int editDistance = getEditDistance(previousService, service);

      if (editDistance < lowestEditDistance)
      {
        lowestEditDistance = editDistance;
        bestMatch = previousService;
      }
    }

    // TODO: jcoon - need to make this service matching threshold configurable
    float maximumAllowableEditDistance = 50;

    if (lowestEditDistance > maximumAllowableEditDistance)
      return null;

    return bestMatch;
  }

  /**
   * Find the edit distance between two Services.
   * 
   * This is the number of changes needed to change one ServiceObject into another, 
   * where each change is a single modification (single character difference between names, etc).
   * 
   * @param previousService
   * @param service
   * @return
   */
  protected static int getEditDistance(ServiceObject previousService, ServiceObject service)
  {
    // name
    int editDistance = KeyGeneratorBase.getEditDistance(previousService.getName(), service.getName());

    //TODO: jcoon - test netscreen service group members?

    // test port range
    editDistance += KeyGeneratorBase.getEditDistance(previousService.getPortRange(), service.getPortRange());

    // test port
    editDistance += KeyGeneratorBase.getEditDistance(previousService.getPort(), service.getPort());

    // test port range end
    editDistance += KeyGeneratorBase.getEditDistance(previousService.getPortRangeEnd(), service.getPortRangeEnd());

    // test src port
    editDistance += KeyGeneratorBase.getEditDistance(previousService.getSrcPort(), service.getSrcPort());

    // test src port range
    editDistance += KeyGeneratorBase.getEditDistance(previousService.getSrcPortRange(), service.getSrcPortRange());

    // test src port range end
    editDistance += KeyGeneratorBase
        .getEditDistance(previousService.getSrcPortRangeEnd(), service.getSrcPortRangeEnd());

    return editDistance;
  }
}
